from gi.repository import Gtk , Gdk, Vte, GLib, Pango
from context_menu import *
from keymap import *
import os
import asyncio
import time
import threading

# 终端内核组件

# 下面的是在VTE里面增加下划线提示的正则表达式
USERCHARS = "-[:alnum:]"
USERCHARS_CLASS = "[" + USERCHARS + "]"
PASSCHARS_CLASS = "[-[:alnum:]\\Q,?;.:/!%$^*&~\"#'\\E]"
HOSTCHARS_CLASS = "[-[:alnum:]]"
HOST = HOSTCHARS_CLASS + "+(\\." + HOSTCHARS_CLASS + "+)*"
PORT = "(?:\\:[[:digit:]]{1,5})?"
PATHCHARS_CLASS = "[-[:alnum:]\\Q_$.+!*,;:@&=?/~#%\\E]"
PATHTERM_CLASS = "[^\\Q]'.}>) \t\r\n,\"\\E]"
SCHEME = """(?:news:|telnet:|nntp:|file:\/|https?:|ftps?:|sftp:|webcal:|irc:|sftp:|ldaps?:|nfs:|smb:|rsync:|ssh:|rlogin:|telnet:|git:|git\+ssh:|bzr:|bzr\+ssh:|svn:|svn\+ssh:|hg:|mailto:|magnet:)"""
USERPASS = USERCHARS_CLASS + "+(?:" + PASSCHARS_CLASS + "+)?"
URLPATH = "(?:(/" + PATHCHARS_CLASS + "+(?:[(]" + PATHCHARS_CLASS + "*[)])*" + PATHCHARS_CLASS + "*)*" + PATHTERM_CLASS + ")?"
REGEX_STRINGS = [
    SCHEME + "//(?:" + USERPASS + "\\@)?" + HOST + PORT + URLPATH,
    "(?:www|ftp)" + HOSTCHARS_CLASS + "*\\." + HOST + PORT + URLPATH,
    "(?:callto:|h323:|sip:)" + USERCHARS_CLASS + "[" + USERCHARS + ".]*(?:" + PORT + "/[a-z0-9]+)?\\@" + HOST,
    "(?:mailto:)?" + USERCHARS_CLASS + "[" + USERCHARS + ".]*\\@" + HOSTCHARS_CLASS + "+\\." + HOST,
    "(?:news:|man:|info:)[[:alnum:]\\Q^_{|}~!\"#$%&'()*+,./;:=?`\\E]+",
    "git\\@" + HOST + ":" + HOST + URLPATH,
]

class Term(Vte.Terminal): 

    def __init__(self,title, notebook, init_dir=None, callback=None): 
        super(Term, self).__init__()
        #绑定参数
        self.title = title
        self.notebook = notebook
        #设置字体，貌似GTK的css不起作用，只能在代码里面设置
        font_desc = Pango.FontDescription("Noto Sans Mono 12")
        self.set_font(font_desc)
        #设置默认启动的shell和默认目录
        home = init_dir or os.getenv('HOME')
        shell = Vte.get_user_shell() or os.getenv('SHELL') or '/bin/sh'
        #绑定事件：
        self.connect("button_press_event",self.on_button_press)
        self.connect("contents-changed", self.on_contents_changed)
        self.connect("selection-changed", self.on_selection_changed)  
        self.connect("key-press-event", self.on_key_press_event)  
        self.connect("window-title-changed", self.on_window_title_changed)  
        self.connect("scroll-event", self.on_scroll_event)  
        self.connect("child-exited", self.on_child_exited)  
        
        #将终端的链接等加上下划线（鼠标经过时）
        self.clickable(REGEX_STRINGS)
        #初始化回调函数
        def _spawn_():
            #VTE初始化
            if not self.spawn(home, [shell]):
                return
            if callback:
                def _f_():
                    callback(self)
                #执行业务回调函数
                GLib.timeout_add(200,_f_)
        #启动
        GLib.idle_add(_spawn_)

    def rename(self,text, force=None):
        self.notebook.rename(self,text,force)

    def spawn(self, home, argv, callback=None):
            if hasattr(self,'spawn_async'):
                try:
                    self.spawn_async(
                        Vte.PtyFlags.DEFAULT,
                        home,
                        argv,
                        None,
                        0,
                        None,
                        callback,
                        -1,
                    )
                    return True
                except BaseException as e:
                    print('-----Spawn1 Error',e)
                    return False
            else:
                try:
                    return self.spawn_sync(
                            Vte.PtyFlags.DEFAULT,
                            home,
                            argv,
                            None,
                            GLib.SpawnFlags.DO_NOT_REAP_CHILD,
                            callback,
                            None,
                        )
                except BaseException as e:
                    print('==Spawn2 Error==',e)
                    return False

    def on_scroll_event(self, term, scroll_event):
        TERMINAL_MIN_OPACITY = 0.2
        if (scroll_event.state & Gdk.ModifierType.CONTROL_MASK) != 0 and (scroll_event.state & Gdk.ModifierType.SHIFT_MASK) == 0:
                try:
                    old_opacity = term.notebook.opacity
                    new_opacity = old_opacity

                    if scroll_event.delta_y < 0:
                        new_opacity = min(max(old_opacity + 0.01, TERMINAL_MIN_OPACITY), 1)
                    elif scroll_event.delta_y > 0:
                        new_opacity = min(max(old_opacity - 0.01, TERMINAL_MIN_OPACITY), 1)

                    if new_opacity != old_opacity:
                        term.notebook.update_opacity(new_opacity)
                    return True
                except GLib.KeyFileError as e:
                    print("Terminal on_scroll: ", e)
        return False

    def on_key_press_event(self, widget, key_event):
        keyevent_name = get_keyevent_name(key_event)
        # print(keyevent_name) #打开这里可以看到每次按的是什么键
        if keyevent_name == "Ctrl + Shift + c":
            self.copy_clipboard() #复制到剪贴板
            return True
        elif keyevent_name == "Ctrl + Shift + v":
            self.paste_clipboard() #从剪贴板黏贴
            return True
        elif keyevent_name == "Ctrl + t":
            self.notebook.new_term() #开一个新的tab
            return True
        elif keyevent_name == "Ctrl + w":
            self.notebook.close_term(self) #关闭当前tab，如果是最后一个，则退出应用
            return True
        elif keyevent_name == "Ctrl + Page_Down":
            self.notebook.next_term() #切换到下一个Tab
            return True
        elif keyevent_name == "Ctrl + Page_Up":
            self.notebook.previous_term() #切换到前一个Tab
            return True
        elif keyevent_name == "Ctrl + Alt + Page_Down":
            self.notebook.move_to_next() #将Tab右移一个位置
            return True
        elif keyevent_name == "Ctrl + Alt + Page_Up":
            self.notebook.move_to_previous() #将Tab左移一个位置
            return True
        elif keyevent_name == "Ctrl + Alt + \\":
            self.popup_menu() #弹出右键菜单（包含有模板子菜单）
            return True
        elif keyevent_name == "Ctrl + Alt + /":
            self.popup_template_menu() #仅仅弹出模板菜单
            return True
        elif keyevent_name == "Ctrl + Alt + Up":
            self.notebook.increase_opacity() #增加透明度
            return True
        elif keyevent_name == "Ctrl + Alt + Down":
            self.notebook.decrease_opacity() #降低透明度
            return True
        elif keyevent_name == "Alt + f":
            self.notebook.window.do_max(None)
            return True
        elif keyevent_name == "Alt + Left":
            self.resize_window(-20,0)
            return True
        elif keyevent_name == "Alt + Right":
            self.resize_window(20,0)
            return True
        elif keyevent_name == "Alt + Up":
            self.resize_window(0,-20)
            return True
        elif keyevent_name == "Alt + Down":
            self.resize_window(0,20)
            return True
        elif keyevent_name == "F2":
            self.notebook.rename_term()
            return True

    def resize_window(self,delta_width, delta_height):
        win = self.notebook.window
        (w,h) = win.get_size()
        win.resize(w+delta_width, h+delta_height)

    #这个事件导致close_term调用了两次
    def on_child_exited(self,widget,event):
        # 如果已经在退出中了，则不再触发close_term函数
        if hasattr(self,'closing') and self.closing==True:
            return True

        self.notebook.close_term(self) #关闭当前tab
        return True

    def on_contents_changed(self, widget):
        (text,attr) = widget.get_text()
        # print(self.title + ' content changed:\r\n', text)
        
    def on_window_title_changed(self, widget):
        self.rename(self.shorten_title(self.get_window_title()))

    #简化窗口标题的路径
    def shorten_title(self, title):
        #将     tony@tonybook:/data/work/gitee/JingTerm
        #简化成：~ok:d~w~g~JingTerm
        try:
            if len(title)<20:
                return title

            host=""
            if ':' in title:
                i = title.index(':')
                a = title[max(0,i-2):i]
                host = a+':'
                b = title[i+1:]
            else:
                b = title
            i=b.rindex('/')
            arr = [x[0] for x in b.split('/')[1:-1] ]
            tail = b[i+1:]
            arr.append( tail[0: min(8,len(tail)) ]  )
            return host + '~'.join(arr)
        except:
            return title

    def on_selection_changed(self, widget):
        #linux下面，如果选中了，复制到主选区（可以通过鼠标中间黏贴）
        if widget.get_has_selection():
            widget.copy_primary()

    def clipboard_has_context(self):
        clipboard_text = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD).wait_for_text()
        return clipboard_text != None and clipboard_text.strip() != ""
        
    def on_button_press(self, widget, event):
        if event.button==Gdk.BUTTON_PRIMARY :
            # 先聚焦窗口
            self.grab_focus()

            # 检查是否有可点击的链接
            (uri,tag) = self.match_check_event(event)
            modifiers = Gtk.accelerator_get_default_mod_mask()
            if (event.state & modifiers) == Gdk.ModifierType.CONTROL_MASK and uri != None:
                try:
                    Gtk.show_uri(None, uri, Gtk.get_current_event_time())
                    return True
                except GLib.Error as error:
                    try :
                        uri = "http://{}".format(uri)
                        Gtk.show_uri(None, uri, Gtk.get_current_event_time())
                    except GLib.Error as error:
                        print("Could Not Open link", error)

        elif event.button==Gdk.BUTTON_SECONDARY:
            self.popup_menu()
            return True

    def popup_menu(self):
        menu = self.create_menus()
        menu.show_all()
        menu.popup_at_pointer()

    def popup_template_menu(self):
        menu = self.create_template_menus()
        menu.show_all()
        menu.popup_at_pointer()

    def create_template_menus(self):
        menu =  Gtk.Menu()
        ContextMenu(menu, self)
        menu.show_all()
        menu.popup_at_pointer()
        return menu

    def create_menus(self):
        menu =  Gtk.Menu()
        a = 0x41
        if self.get_has_selection():
            self.menu_copy = Gtk.MenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + "Copy")
            a += 1
            self.menu_copy.connect("activate", self.menu_clicked)
            menu.add(self.menu_copy)
        self.menu_reset = Gtk.MenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + "Reset")
        a += 1
        self.menu_reset.connect("activate", self.menu_clicked)
        menu.add(self.menu_reset)

        if self.clipboard_has_context():
            self.menu_paste = Gtk.MenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + "Paste")
            a += 1
            self.menu_paste.connect("activate", self.menu_clicked)
            menu.add(self.menu_paste)

        self.menu_paste_primary = Gtk.MenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + "Paste Primary")
        a += 1
        self.menu_paste_primary.connect("activate", self.menu_clicked)
        menu.add(self.menu_paste_primary)

        self.menu_rename = Gtk.MenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + "Rename")
        a += 1
        self.menu_rename.connect("activate", self.menu_clicked)
        menu.add(self.menu_rename)

        self.menu_scroll = Gtk.CheckMenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + 'Scroll on output')
        a += 1
        self.menu_scroll.props.active = self.props.scroll_on_output
        self.menu_scroll.connect("toggled", self.menu_clicked) #必须要先设置active属性，然后再绑定toggled事件，否则会多触发一次！
        menu.add(self.menu_scroll)

        menu_template_list = Gtk.Menu()
        menu_template = Gtk.MenuItem.new_with_mnemonic('_' + chr(a)+ ': ' + "Templates")
        a += 1
        ContextMenu(menu_template_list, self)
        menu_template.set_submenu(menu_template_list)
        menu.add(menu_template)
        return menu


    def menu_clicked(self, menuitem):
        if menuitem==self.menu_reset:
            self.reset(True,True)
            self.feed_child('\r'.encode('utf-8'))
        elif hasattr(self,'menu_paste') and menuitem==self.menu_paste:
            self.paste_clipboard()
        elif menuitem==self.menu_paste_primary:
            self.paste_primary ()
        elif menuitem==self.menu_rename:
            self.notebook.rename_term()
        elif hasattr(self,'menu_copy') and menuitem==self.menu_copy:
            self.copy_clipboard()
        elif menuitem==self.menu_scroll:
            self.props.scroll_on_output = not self.props.scroll_on_output
        return True

    #获取当前目录
    def get_cwd(self):
        pty = self.get_pty()
        if pty :
            pty_fd = pty.props.fd
            fpid = os.tcgetpgrp(pty_fd)
            if fpid > 0:
                current_dir = os.readlink("/proc/{}/cwd".format(fpid))
                return current_dir
        return None

    #自动向vte输入内容
    def enter(self,content):
        self.feed_child(content.encode('utf-8'))

    #自动向vte输入内容并回车
    def enterln(self,content):
        self.feed_child((content+"\r").encode('utf-8'))

    #顺序执行命令
    def run_commands(self,contents):
        ENTERLN='enterln:'
        ENTER='enter:'
        SLEEP='sleep:'
        TITLE='title:'
        for i in range(0,len(contents)):
            c = contents[i]
            if c.startswith(ENTERLN):
                self.enterln(c[len(ENTERLN):])
            elif c.startswith(ENTER):
                self.enterln(c[len(ENTER):])
            elif c.startswith(TITLE):
                self.rename(c[len(TITLE):],True)
            elif c.startswith(SLEEP):
                time.sleep(float(c[len(SLEEP):]))

    #启用独立线程，执行一批命令，以免互相干扰
    def start_commands_thread(self, commands):
        def _f():
            self.run_commands(commands)
        threading.Thread(target=_f).start()

    #设置vte中的可以点击的正则表达式
    def clickable(self,str_arr):
        for exp in str_arr:
            try:
                regex = Vte.Regex.for_match(exp, -1, 0x00000400) # /* PCRE2_MULTILINE */
                id = self.match_add_regex(regex, 0)
                self.match_set_cursor_type(id, Gdk.CursorType.HAND2)
            except BaseException as e:
                try:
                    regex = GLib.Regex(exp,
                                        GLib.RegexCompileFlags.OPTIMIZE |
                                        GLib.RegexCompileFlags.MULTILINE,
                                        0)
                    id = self.match_add_gregex(regex, 0)
                    self.match_set_cursor_type(id, Gdk.CursorType.HAND2)
                except BaseException as e:
                    print(e)
                
